/*
 This file is part of Desu: MapleStory v62 Server Emulator
 Copyright (C) 2014  Brent Rinchiuso <brentrin@gmail.com>

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package field;

import client.packet.PacketCreator;
import field.FieldObject.Type;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;
import net.Packet;
import player.Player;
import player.community.Party;
import wz.WzObject;

/**
 *
 * @author Brent Rinchiuso
 */
public final class Field {

    private int id;
    private boolean fly;
    private boolean swim; // for anti-hack
    private boolean town;
    private int returnField;
    private float mobRate;
    private int fieldType;
    private int forcedReturn;
    private String onFirstUserEnter;
    private String onUserEnter;
    private int timeLimit;
    private final AtomicInteger runningOid;
    private LinkedList<Spawner> spawns;
    private ArrayList<Portal> portals;
    private LinkedList<Reactor> reactors;
    private Map<Integer, Monster> monsters;
    private Map<Integer, FieldObject> objects;
    private Supplier<Stream<Player>> playerSupplier;
    public static final int CHARACTER_VIEW_DISTANCE = 722500;

    public Field(int id) {
        this.id = id;
        spawns = new LinkedList<>();
        reactors = new LinkedList<>();
        objects = Collections.synchronizedMap(new HashMap<Integer, FieldObject>());
        monsters = Collections.synchronizedMap(new HashMap<Integer, Monster>());
        runningOid = new AtomicInteger(0);
        playerSupplier = () -> objects.values().stream().filter(o -> o.getObjectType().equals(Type.PLAYER)).map(o -> (Player) o);
    }
    
    public Stream<Player> getPlayers() {
        return playerSupplier.get();
    }

    public void purge() { // XXX convert to returning a list of players?
        spawns.clear();
        portals.clear();
        reactors.clear();
        monsters.clear();
        objects.clear();
        // moves this into a dead-map state and will be collected hopefully
    }

    public int getFieldId() {
        return id;
    }

    public void forceFieldId(int id) {
        this.id = id;
    }

    public void setTown(boolean town) {
        this.town = town;
    }

    public boolean isTown() {
        return town;
    }

    public void setFlyEnabled(boolean fly) {
        this.fly = fly;
    }

    public boolean isFlyEnabled() {
        return fly;
    }

    public void setOnFirstUserEnter(String s) {
        onFirstUserEnter = s;
    }

    public String getOnFirstUserEnter() {
        return onFirstUserEnter;
    }

    public void setOnUserEnter(String s) {
        onUserEnter = s;
    }

    public String getOnUserEntry() {
        return onUserEnter;
    }

    public void setForcedReturn(int id) {
        forcedReturn = id;
    }

    public int getForcedReturn() {
        return forcedReturn;
    }

    public void setTimeLimit(int timeLimit) {
        this.timeLimit = timeLimit;
    }

    public int getTimeLimit() {
        return timeLimit;
    }

    public void setReturnField(int returnField) {
        this.returnField = returnField;
    }

    public int getReturnField() {
        return returnField;
    }

    public void setMobRate(float mobRate) {
        this.mobRate = mobRate;
    }

    public float getMobRate() {
        return mobRate;
    }

    public void setSwimEnabled(boolean swim) {
        this.swim = swim;
    }

    public boolean isSwimEnabled() {
        return swim;
    }
    
    public void setFieldType(int ft) {
        fieldType = ft;
    }
    
    public int getFieldType() {
        return fieldType;
    }

    public void addFieldObject(FieldObject mmo) {
        mmo.setOid(nextOid());
        objects.put(mmo.getOid(), mmo);
    }

    public void removeFieldObject(FieldObject mmo) {
        objects.remove(mmo.getOid());
    }

    public FieldObject getFieldObject(int oid) {
        return objects.get(oid);
    }

    public void addSpawner(Spawner sp) {
        spawns.add(sp);
    }

    public void parsePortalList(WzObject<?, ?> par) {
        portals = new ArrayList<>(par.getChildren().size());
        for (int i = 0; i < par.getChildren().size(); i++) {
            portals.add(null);
        }
        for (WzObject o : par) {
            int pid = Integer.valueOf(o.getName());
            portals.set(pid, new Portal(pid, o));
        }
        portals.trimToSize();
    }

    public void addReactor(Reactor mr) {
        reactors.add(mr);
    }

    private int nextOid() {
        if (runningOid.incrementAndGet() > 2000000000) {
            runningOid.set(1000);
        }
        if (objects.containsKey(runningOid.get())) {
            while (objects.containsKey(runningOid.incrementAndGet()));
        }
        return runningOid.get();
    }

    public void spawnMonster(Monster mob) {
        mob.setOid(nextOid());
        objects.put(mob.getOid(), mob);
        monsters.put(mob.getOid(), mob);
        
        getPlayers().forEach(p -> {
            mob.sendSpawnData(p.getClient());
            if (mob.getController() == FieldLife.NO_CONTROLLER) {
                mob.giveControl(p);
            }
        });
    }

    public void killMonster(Monster mob, int killer, Map<Integer, AtomicInteger> dist) { // XXX kill monster, remove control from player
        int control = mob.getController();
        FieldObject controller = objects.get(control);
        mob.removeControl((Player) controller);
        
        getPlayers().forEach(p -> mob.sendDestroyData(p.getClient(), true));

        objects.remove(mob.getOid());
        monsters.remove(mob.getOid());
        
        int exp = mob.getExperience();
        
        int totalHealth = mob.getMaxHP();
        
        Map<Integer, Integer> expDist = new HashMap<>();
        
        Map<Integer, Integer> partyExp = new HashMap<>();
        
        // 80% of pool is split amongst all the damagers
        dist.forEach((p, d) -> expDist.put(p, (int) (0.80f * Math.floorDiv(exp * d.get(), totalHealth)))); 
        
        // expDist is known for each player assuming no party, collect EXP into parties
        
        getPlayers().filter(p -> expDist.containsKey(p.getOid())).forEach(p -> {
            boolean isKiller = p.getOid() == killer;
            int xp = expDist.get(p.getOid());
            if (isKiller) { // killer gives flat 20%  to party pool/individual yield
                xp += Math.floorDiv(exp, 5);
            }
            Party party = p.getParty();
            if (party != null) {
                int pID = party.getId();
                int pXP = xp + (partyExp.containsKey(pID) ? partyExp.get(pID) : 0);
                partyExp.put(pID, pXP);
            } else {
                if (!mob.isBoss() && isKiller) {
                    p.incrementMonsterKills();
                }
                p.gainExp(xp, isKiller);
            }
        });
        
        if (!partyExp.isEmpty()) {
            partyExp.forEach((i, x) -> {
                Player target = getPlayers().sequential(). // force sequential execution of pipeline to avoid NPE at 2nd filter
                        filter(p -> p.getParty() != null).
                        filter(p -> p.getParty().getId() == i).
                        findAny().orElse(null); // just to avoid exception since if optional value is null, an exception will be thrown
                if (target != null) {
                    target.getParty().distributeExp(x, id, killer, mob.isBoss(), mob.getLevel(), totalHealth, partyExp);
                }
            });
        }
        
        checkRespawn(false); // hue
    }

    public void spawnPlayer(Player p) {
        spawnPlayer(p, true);
    }

    public void spawnPlayer(Player p, boolean spawn) {
        if (spawn) {
            p.setPosition(getSpawnLocation(p.getSpawnpoint()));
        }
        // XXX onUserEnter junk
        // XXX field limitations
        // XXX pets
        broadcast(PacketCreator.spawnPlayer(p), p.isHidden());
        
        addFieldObject(p);
        
        Supplier<Stream<FieldObject>> viewable = () -> objects.values().stream().filter(o -> !o.isHidden());
        
        Supplier<Stream<FieldObject>> visible = () -> viewable.get().filter(o -> !o.isNonRanged());
        
        viewable.get().filter(o -> o.isNonRanged()).forEach(o -> o.sendSpawnData(p.getClient()));
        
        viewable.get().filter(o -> o.getObjectType().equals(Type.NPC)).map(o -> (NPC) o).forEach(n -> updateController(n, p));
        
        visible.get().filter(o -> o.getObjectType().equals(Type.MONSTER)).map(o -> (Monster) o).forEach(m -> updateController(m, p));
        
        visible.get().filter(o -> inRange(p.getPosition(), o.getPosition())).forEach(o -> {
            o.sendSpawnData(p.getClient());
            p.addVisibleObject(o.getOid());
        });
        
        reactors.stream().filter(r -> (!r.isHidden() && inRange(p.getPosition(), r.getPosition()))).
                forEach(r -> 
                {
                    r.sendSpawnData(p.getClient());
                    p.addVisibleObject(r.getOid());
                });
        if (fieldType > 0) { // for maple island tutorial, sauna, etc
            p.getClient().write(PacketCreator.forceEquip());
        }
        // XXX spawn summon
        // XXX events
        // XXX clock
        // XXX boat
        checkRespawn(false);
    }

    private void checkRespawn(boolean forceSpawn) {
        int shouldSpawn = (spawns.size() - monsters.size());
//        mobRate -> not sure how exponential this
//        actually makes mob spawns, going to see
//        if we should use this in some sort of calc
//        like: 1 + floor(mobRate / 100)
        if (shouldSpawn > 0) {
            Collections.shuffle(spawns);
            int count = 0;
            for (Spawner sp : spawns) {
                if (sp.shouldSpawn()) {
                    sp.respawn(this, false);
                }
                count++;
                if (count >= shouldSpawn) {
                    break;
                }
            }
        }
        if (forceSpawn) {
            if (monsters.isEmpty()) {
                Collections.shuffle(spawns);
                int count = 0;
                int force = spawns.size() / 2;
                for (Spawner sp : spawns) {
                    sp.respawn(this, true);
                    count++;
                    if (count >= force) {
                        break;
                    }
                }
            }
        }
    }
    
    private static boolean inRange(Point from, Point to) {
        return inRange(from, to, CHARACTER_VIEW_DISTANCE);
    }

    private static boolean inRange(Point from, Point to, double range) {
        return from.distanceSq(to) <= range;
    }

    private Point getSpawnLocation(int spawn) {
        Portal ret = portals.get(spawn);
        if (ret != null && ret.getPn().equals("sp")) {
            return ret.getPosition();
        } else {
            for (Portal p : portals) { // first portal we find that is spawn
                if (p != null && p.getPn().equals("sp")) {
                    return p.getPosition();
                }
            }
        }
        if (!portals.isEmpty()) {
            return portals.get(0).getPosition();
        } else {
            return new Point(0, 0); // XXX find position by foothold, this shouldn't happen
        }
    }

    public int getClosestSpawn(Player c) {
        int closest = -1;
        double dist = 0xFFFFFFFF;
        for (int i = 0; i < portals.size(); i++) {
            Portal p = portals.get(i);
            if (p != null && p.getPn().equals("sp")) {
                if (closest != -1) {
                    double cdist = p.getPosition().distance(c.getPosition());
                    if (cdist < dist) {
                        dist = cdist;
                        closest = i;
                    }
                } else {
                    closest = i;
                }
            }
        }
        return closest == -1 ? 0 : closest;
    }

    private void findNewController(int oid) {
        Player p = getPlayers().findFirst().orElse(null);
        if (p != null) {
            FieldObject o = getFieldObject(oid);
            if (o instanceof NPC || 
                    o instanceof Monster) {
                updateController((FieldLife) o, p);
            }
        }
    }
    
    private void updateController(FieldLife l, Player p) {
        if (l.getController() == FieldLife.NO_CONTROLLER) {
            l.giveControl(p);
        }
    }

    public void removePlayer(Player p) {
        List<Integer> toRemove = new ArrayList<>(p.getControlled());
        
        for (Integer r : toRemove) {
            FieldObject o = getFieldObject(r);
            if (o != null) {
                if (o instanceof FieldLife) {
                    ((FieldLife) o).removeControl(p);
                }
            }
        }
        removeFieldObject(p);
        
        toRemove.stream().forEach(target -> findNewController(target));
        
        getPlayers().forEach(o -> p.sendDestroyData(o.getClient()));
    }

    public void updatePlayerView(Player p) {
        Integer[] visible = p.getVisibleObjects().toArray(new Integer[0]);
        for (Integer i : visible) {
            if (objects.containsKey(i)) {
                updateObjectVisibility(p, i);
            }
        }
        objects.values().stream().filter(o -> !o.isNonRanged() && !p.canSee(o.getOid()) && inRange(p.getPosition(), o.getPosition())).forEach(o -> 
        {
            o.sendSpawnData(p.getClient());
            p.addVisibleObject(o.getOid());
        });
    }

    private void updateObjectVisibility(Player p, int oid) {
        FieldObject o = objects.get(oid);
        if (!p.canSee(oid) && !o.isNonRanged()) {
            if (o.getObjectType().equals(Type.SUMMON)
                    || inRange(p.getPosition(), o.getPosition())) {
                p.addVisibleObject(oid);
                o.sendSpawnData(p.getClient());
                if (o.getObjectType().equals(Type.MONSTER)) {
                    Monster mob = (Monster) o;
                    updateController(mob, p);
                }
            }
        } else if (!o.isNonRanged()) {
            if (!o.getObjectType().equals(Type.SUMMON)
                    && !inRange(p.getPosition(), o.getPosition())) {
                p.removeVisibleObject(oid);
                o.sendDestroyData(p.getClient());
                // XXX check to make sure even if 
            }
        }
    }

    public void broadcast(Packet p) {
        broadcast(p, false, -1);
    }

    public void broadcast(Packet p, int... exclude) {
        broadcast(p, false, exclude);
    }

    public void broadcast(Packet p, boolean gm) {
        broadcast(p, gm, -1);
    }

    public void broadcast(Packet p, boolean gm, int... exclude) { // may need an option for range box
        for (FieldObject o : objects.values()) {
            if (o instanceof Player) {
                Player t = (Player) o;
                boolean skip = false;
                for (int i = 0; i < exclude.length; i++) {
                    if (exclude[i] == t.getId()) {
                        skip = true;
                        break;
                    }
                }
                if (!skip) {
                    if (!gm || (gm && t.isGM())) {
                        t.getClient().write(p.clone());
                    }
                }
            }
        }
    }

    public Portal getPortal(int portal) {
        return portals.get(portal);
    }

    public Portal getPortalByName(String pn) {
        for (Portal mp : portals) {
            if (mp.getPn().equals(pn)) {
                // this isn't accurate if there's multiple
                // portals with the same name
                return mp;
            }
        }
        return null;
    }
}
